local _G = getfenv(0)
_G.NRT = AceLibrary("AceAddon-2.0"):new(
	"AceEvent-2.0",
	"AceDB-2.0",
	"AceConsole-2.0",
	"FuBarPlugin-2.0"
)
local addon = _G.NRT
addon.revision = tonumber(("$Revision: 127 $"):sub(12, -3))
addon.version = (addon.version or "3") .. addon.revision

-- We call these so much during the tooltip updating that I local'd them here.
local ipairs = ipairs
local unpack = unpack
local select = select
local type = type
local tonumber = tonumber
local tostring = tostring
local table_insert = table.insert
local table_sort = table.sort
local date = date

-----------------------------------------------------------------------
-- START CUSTOMIZATION SECTION
--
-- Everything here can be customized by you easily. Everything should explain
-- itself or have a pretty detailed comment explaining what happens when you
-- edit it.
--
-- Note that all the output formatters are added at the end of the file and can
-- be customized there. This section is for the UI and tracking.
--

-- The data columns available for the tooltip UI.
-- Adding new columns are easy, just add them here and then create your own data
-- function for it in the columnTextFunctions table that you find later in the
-- file. Note that you don't really have to edit the columnPriority or
-- columnColors tables at all, but you can if you want to.
local attendanceColumns = { "Date", "Attendants", "Tracked", "Note" }
local lootColumns = { "Date", "Player", "Item", "From", "Note" }

-- Just a few variables that the table functions depend on, declared later in the file.
local coloredName

-- If you add your own columns above, you have to add a function here so that
-- the UI knows how to get data for that column. The input variable to each
-- function is basically the same data you get to the output formatters at the
-- end of the file. I suggest you just fool around.
local columnTextFunctions = {
	attendance = {
		Date = function(input) return date("%d/%m %H:%M", input.date) end,
		Attendants = function(input) return #input.attendants end,
		Tracked = function(input) return input.name end,
		Note = function(input) return input.note end,
	},
	loot = {
		Date = function(input) return date("%d/%m %H:%M", input.date) end,
		Player = function(input) return coloredName[input.player] end,
		Item = function(input) return input.item end,
		From = function(input) return input.from end,
		Note = function(input)
			local id = input.itemId
			if not id then
				id = tonumber(select(3, input.item:find("item:(%d+):")))
			end
			return addon.db.profile.itemNotes[id]
		end,
	},
}

-- Lower priority columns are to the left side of the tooltip, higher priority
-- columns are to the right.
local columnPriority = setmetatable({
	Date = 1,
	Attendants = 2,
	Tracked = 3,
	Player = 4,
	Item = 5,
	From = 6,
	Note = 7,
},{__index = function(self, key)
		local max = 0
		for k, v in pairs(self) do
			if v > max then max = v end
		end
		self[key] = max + 1
		return self[key]
	end
})

-- RGB table colors from 0 to 1. i.e. {1,0,0} is fully red.
-- If you don't fill in a color for a column, the default yellow color is used.
-- Note that if two colums have the same name for both the attendance and loot
-- lists, they must have the same color. Like the Date and Note columns.
local columnColors = {
	Date = {1, 1, 0},
	Tracked = {1, 1, 1},
	Note = {1, 1, 1},
}

-- These items are always ignored by the loot handler.
local ignoredItems = {
	[30316] = true, -- Devastation
	[30317] = true, -- Cosmic Infuser
	[30312] = true, -- Infinity Blade
	[30318] = true, -- Netherstrand Longbow
	[30313] = true, -- Staff of Disintegration
	[30311] = true, -- Warp Slicer
	[30320] = true, -- Bundle of Nether Spikes
	[30314] = true, -- Phaseshift Bulwark
	[30319] = true, -- Nether Spikes
	[29434] = true, -- Badge of Justice
	[40752] = true, -- Emblem of Heroism
	[40753] = true, -- Emblem of Valor
	[22450] = true, -- Void Crystal
	[20725] = true, -- Nexus Crystal
	[34057] = true, -- Abyss Crystal
}

-- These items are tracked in a "Special Items" menu, regardless of their
-- quality. Items listed here will never be added to the standard "looted by"
-- list.
local specialItems = {
	[32897] = true, -- Mark of the Illidari
	[32428] = true, -- Heart of Darkness
	[34664] = true, -- Sunmote

	-- Epic gems
	[32230] = true, -- Shadowsong Amethyst
	[32249] = true, -- Seaspray Emerald
	[32231] = true, -- Pyrestone
	[32229] = true, -- Lionseye
	[32228] = true, -- Empyrean Sapphire
	[32227] = true, -- Crimson Spinel
}

-- The bosses listed here are always ignored and when they are killed, you won't
-- get a "boss dead, take attendance now?" popup box.
local defaultIgnoredBosses = {
	"Krosh Firehand",
	"Olm the Summoner",
	"Kiggler the Crazed",
	"Blindeye the Seer",
	"Thaladred the Darkener",
	"Master Engineer Telonicus",
	"Grand Astromancer Capernian",
	"Lord Sanguinar",
	"Hellfire Channeler",
	"Amani Bear Spirit",
	"Amani Lynx Spirit",
	"Amani Eagle Spirit",
	"Drek'Thar",
	"Vanndar Stormpike",
	"Captain Galvangar",
	"Captain Balinda Stonehearth",
	"Madrigosa",
	"Tenebron",
	"Shadron",
	"Vesperon",
}
local ignoredBosses = {} -- The translated results from Babble-Boss.

-- The following code translates the bosses in ignoredBosses to your
-- current locale.
-- If you're not on an english client, remember that Babble-Boss doesn't
-- actually include all the boss names in the game at the moment. Things like
-- Zul'jins different forms are not included there, so you have to translate
-- those directly into defaultIgnoredBosses if you care.
local bb = LibStub("LibBabble-Boss-3.0"):GetUnstrictLookupTable()
for i, v in ipairs(defaultIgnoredBosses) do
	if bb[v] then
		ignoredBosses[bb[v]] = true
	else
		ignoredBosses[v] = true
	end
end

-----------------------------------------------------------------------
-- END CUSTOMIZATION SECTION
--

local deformat = AceLibrary("Deformat-2.0")
local tablet = AceLibrary("Tablet-2.0")

-- Reference to our attendance tracker details frame, also used to show lists of
-- special items.
local trackerFrame = nil

-- Functions
local setFrameHeader = nil
local setText = nil
local createTrackerFrame = nil

-- State variables
local currentTrackset = nil
local grouped = nil
local lastBossKilled = nil
local updateUIColumnList = true
local waitingList = {}

local guildRanks = {}
local memberRanks = {}
local guildMemberList = {}

local outputFormatters = {
	-- The different types of output formatters available.
}
local formatters = {
	-- Keyed lookup table holding the references to the formatters.
}

local killedBosses = {
	-- Lists the bosses killed since session start, not counting ignored bosses.
}

local worldbosses = {
	-- Holds a map of the worldboss class mobs we've seen since login.
}

-- Helper table to cache hex colors for all classes.
local hexColors = setmetatable({}, {__index =
	function(self, key)
		if RAID_CLASS_COLORS[key] then
			local c = RAID_CLASS_COLORS[key]
			self[key] = "|cff" .. string.format("%02x%02x%02x", c.r * 255, c.g * 255, c.b * 255)
			return self[key]
		else
			return "|cffa0a0a0"
		end
	end
})

-- Helper table to cache colored player names.
coloredName = setmetatable({}, {__index =
	function(self, key)
		if type(key) == "nil" then return nil end
		local class = guildMemberList[key] or select(2, UnitClass(key))
		if class then
			self[key] = hexColors[class] .. key .. "|r"
			return self[key]
		else
			return hexColors[key] .. key .. "|r"
		end
	end
})

-- We'll worry about locales when someone wants to translate it :)
local L = setmetatable({}, {__index =
	function(self, key)
		self[key] = key
		return key
	end
})

local function get(key)
	return addon.db.profile[key]
end
local function set(key, val)
	addon.db.profile[key] = val
end
local function validatePlayerName(input)
	if type(input) ~= "string" then return false end
	if input:len() < 2 then return false end
	-- We can't use %A, since it "letters" in Lua doesn't include anything
	-- except A-Za-z ...
	--if input:find("%A") then return false end
	if input:find("%d") or input:find("%s") or input:find("%p") then return false end
	input = input:lower()
	if input == "add" or input == "spacer" then return false end
	return true
end

local options
options = {
	type = "group",
	args = {
		attendance = {
			type = "header",
			name = L["Attendance"],
			order = 100,
		},
		announce = {
			type = "group",
			name = L["Announce"],
			desc = L["Options for attendance announcements."],
			order = 101,
			args = {
				enable = {
					type = "toggle",
					name = L["Enable"],
					desc = L["Enable or disable auto-announcing when taking attendance.\n\nNote that if this option is disabled, people will not be able to whisper you to be added."],
					get = get,
					set = set,
					passValue = "announceAttendance",
					order = 1,
				},
				guild = {
					type = "toggle",
					name = L["Guild"],
					desc = L["Announce when taking attendance to the guild."],
					get = get,
					set = set,
					passValue = "guildAttendance",
					order = 2,
					disabled = function() return not addon.db.profile.announceAttendance end,
				},
				custom = {
					type = "text",
					name = L["Custom channel"],
					desc = L["Announce when taking attendance to a custom channel as well."],
					usage = L["<channelName or blank to not announce>"],
					get = function()
						return addon.db.profile.customAttendance
					end,
					set = function(v)
						if type(v) == "string" then v = v:trim() end
						if type(v) == "string" and v:len() > 0 then
							addon.db.profile.customAttendance = v
						else
							addon.db.profile.customAttendance = nil
						end
					end,
					order = 3,
					disabled = function() return not addon.db.profile.announceAttendance end,
				},
			},
		},
		announceTimeout = {
			type = "range",
			name = L["Timeout"],
			desc = L["How long, in minutes, the timeout should be from when you take attendance until people can no longer whisper you."],
			order = 102,
			min = 1,
			max = 10,
			step = 1,
			get = get,
			set = set,
			passValue = "announceTimeout",
			disabled = function() return not addon.db.profile.announceAttendance end,
		},
		add = {
			type = "text",
			name = L["Add player"],
			desc = L["Adds a player to the most recent attendance set."],
			order = 103,
			get = false,
			usage = L["<player name>"],
			set = function(newPlayer)
				newPlayer = newPlayer:lower():gsub("^%l", string.upper)
				local size = #addon.db.profile.tracked
				local set = addon.db.profile.tracked[size]
				if set then
					local found = nil
					for i, v in ipairs(set.attendants) do
						if v == newPlayer then
							found = true
							break
						end
					end
					if not found then
						table_insert(set.attendants, newPlayer)
						addon:Print(L["%q added to attendance set %d."]:format(newPlayer, size))
					end
				end
			end,
			validate = validatePlayerName,
			disabled = function() return type(addon.db.profile.tracked) ~= "table" or #addon.db.profile.tracked == 0 end,
		},
		filters = {
			type = "group",
			name = L["Filters"],
			desc = L["Configure filters for attendance checks."],
			order = 104,
			args = {
				ranks = {
					type = "text",
					name = L["Guild ranks"],
					desc = L["Only allow guild members of the selected ranks to be part of attendance sets."],
					order = 1,
					get = function(key)
						return not addon.db.profile.disallowRanks[key]
					end,
					set = function(key, val)
						addon.db.profile.disallowRanks[key] = not val
						if val then
							addon:Print(L["Members of rank %q will be allowed on attendance sets."]:format(key))
						else
							addon:Print(L["Members of rank %q will NOT be allowed on attendance sets."]:format(key))
						end
					end,
					multiToggle = true,
					validate = guildRanks,
					disabled = function() return not IsInGuild() end,
				},
				groups = {
					type = "range",
					name = L["Groups"],
					desc = L["Only include people in the groups up to the number set here. Defaults to 5, which means that only people in group 1-5 will be part of the default attendance snapshot, so you can use groups 6-8 as waiting list groups."],
					order = 2,
					min = 1,
					max = 8,
					step = 1,
					get = get,
					set = set,
					passValue = "groups",
				},
				whitelist = {
					type = "group",
					name = L["Whitelist"],
					desc = L["Players added to the whitelist will be accepted on the attendance sets without being in your guild.\n\nClick a name to remove it from the list."],
					order = 3,
					args = {
						spacer = {
							type = "header",
							name = " ",
							order = 50,
							hidden = function() return not next(addon.db.profile.whitelist) end
						},
						add = {
							type = "text",
							name = L["Add"],
							desc = L["Type in the name of the player to whitelist."],
							usage = L["<player name>"],
							get = false,
							set = function(newPlayer)
								newPlayer = newPlayer:lower():gsub("^%l", string.upper)
								addon.db.profile.whitelist[newPlayer] = true
								addon:UpdateWhitelist()

								addon:Print(L["%q added to the whitelist."]:format(newPlayer))
							end,
							validate = validatePlayerName,
							order = 100,
						},
					},
				},
				blacklist = {
					type = "group",
					name = L["Blacklist"],
					desc = L["Players added to the blacklist will never be accepted on the attendance sets.\n\nClick a name to remove it from the list."],
					order = 4,
					args = {
						spacer = {
							type = "header",
							name = " ",
							order = 50,
							hidden = function() return not next(addon.db.profile.blacklist) end
						},
						add = {
							type = "text",
							name = L["Add"],
							desc = L["Type in the name of the player to blacklist. The name is case-insensitive."],
							usage = L["<player name>"],
							get = false,
							set = function(newPlayer)
								newPlayer = newPlayer:lower():gsub("^%l", string.upper)
								addon.db.profile.blacklist[newPlayer] = true
								addon:UpdateBlacklist()

								addon:Print(L["%q added to the blacklist."]:format(newPlayer))
							end,
							validate = validatePlayerName,
							order = 100,
						},
					},
				},
			},
		},
		waitlist = {
			type = "text",
			name = L["Waiting list"],
			desc = L["A list of all the people who have whispered you during attendance this session.\n\nClick a name to invite them to your group and remove them from the list. Shift-Click to remove them from the list."],
			order = 106,
			get = false,
			set = function(key)
				for i, v in ipairs(waitingList) do
					if v == key then
						table.remove(waitingList, i)
						break
					end
				end

				if not IsShiftKeyDown() then
					-- Remove any coloring from the name before we invite.
					local name = key:gsub("(|c%x%x%x%x%x%x%x%x)", "")
					name = name:gsub("(|r)", "")
					InviteUnit(name)
				end
			end,
			multiToggle = true,
			validate = waitingList,
			disabled = function() return #waitingList == 0 end,
		},
		boss = {
			type = "toggle",
			name = L["On boss kill"],
			desc = L["Take attendance when a boss is killed."],
			order = 107,
			get = get,
			set = set,
			passValue = "bossKill",
		},
		onLoot = {
			type = "toggle",
			name = L["On loot"],
			desc = L["Take attendance when an item that is not in the special items list and not ignored is looted."],
			order = 108,
			get = get,
			set = set,
			passValue = "attendanceOnLoot",
		},
		suppressPopup = {
			type = "toggle",
			name = L["Suppress popup box"],
			desc = L["Suppress the attendance popup on boss kills and loot, and automatically take attendance instead."],
			order = 109,
			get = get,
			set = set,
			passValue = "suppressPopup",
		},
		spacer1 = {
			type = "header",
			name = " ",
			order = 150,
		},
		lootOptions = {
			type = "header",
			name = L["Loot"],
			order = 200,
		},
		announceLoot = {
			type = "toggle",
			name = L["Announce loot to guild"],
			desc = L["Automatically announce to guild chat when someone loots an epic item that is not ignored."],
			order = 201,
			get = get,
			set = set,
			passValue = "announceLoot",
		},
		autoLootNote = {
			type = "toggle",
			name = L["Always add loot note"],
			desc = L["Always pop up the loot note dialog when a non-special non-ignored item is looted."],
			order = 202,
			get = get,
			set = set,
			passValue = "autoLootNote",
		},
		attachToBoss = {
			type = "toggle",
			name = L["Attach trash drops to last boss"],
			desc = L["Attach all trash epic drops except special and ignored items to the last boss killed instead of the zone.\n\nIf no boss has been killed yet, it will still be attached to the zone."],
			order = 203,
			get = get,
			set = set,
			passValue = "attachTrashToBoss",
		},
		spacer2 = {
			type = "header",
			name = " ",
			order = 250,
		},
		otherOptions = {
			type = "header",
			name = L["Other options"],
			order = 300,
		},
		special = {
			type = "group",
			name = L["Special items"],
			desc = L["Lists the special items looted during raids."],
			order = 301,
			args = {},
			disabled = function()
				return not next(options.args.special.args)
			end,
		},
		formatter = {
			type = "text",
			name = L["Output format"],
			desc = L["Choose the output format for attendance details."],
			order = 302,
			get = get,
			set = set,
			passValue = "formatter",
			validate = outputFormatters,
		},
		columns = {
			type = "group",
			name = L["Columns"],
			desc = L["Configure which columns should be shown in the tooltip."],
			order = 303,
			args = {
				attendance = {
					type = "text",
					name = L["Attendance"],
					desc = L["Set which columns should be shown for the attendance list."],
					get = function(key)
						return addon.db.profile.columns.attendance[key]
					end,
					set = function(key, val)
						updateUIColumnList = true
						addon.db.profile.columns.attendance[key] = val
					end,
					validate = attendanceColumns,
					multiToggle = true,
				},
				loot = {
					type = "text",
					name = L["Loot"],
					desc = L["Set which columns should be shown for the loot list."],
					get = function(key)
						return addon.db.profile.columns.loot[key]
					end,
					set = function(key, val)
						updateUIColumnList = true
						addon.db.profile.columns.loot[key] = val
					end,
					validate = lootColumns,
					multiToggle = true,
				},
			},
		},
		tenMan = {
			type = "toggle",
			name = L["Enable in 10-man"],
			desc = L["Enable NRT in 10-man instances or not. If you toggle this option off, NRT will only enable itself when your group reaches 11 or more members."],
			order = 304,
			get = get,
			set = set,
			passValue = "tenMan",
		},
		spacer3 = {
			type = "header",
			name = " ",
			order = 350,
		},
		clearAll = {
			type = "execute",
			name = L["Clear all"],
			desc = L["Clears all the recorded information, including loot, special items and attendance sets."],
			order = 400,
			func = "ClearAll",
			confirm = true,
		},
	},
}

addon.hasIcon = "Interface\\Icons\\Spell_Holy_DivineIntervention"

-----------------------------------------------------------------------
-- Initialization

function addon:OnInitialize()
	self.clickableTooltip = true
	self.hasNoColor = true
	self.hideMenuTitle = true
	self.independentProfile = true

	self:RegisterDB("NRTDB")
	self:RegisterDefaults("profile", {
		loot = {},
		tracked = {},
		specialItems = {},
		customAttendance = "nstandby",
		guildAttendance = true,
		announceAttendance = true,
		bossKill = true,
		attendanceOnLoot = nil,
		announceLoot = true,
		whitelist = {},
		blacklist = {},
		trackerFrameWidth = 280,
		trackerFrameHeight = 420,
		formatter = "Nihilum",
		dbFormat = 2,
		announceTimeout = 2,
		groups = 5,
		itemNotes = {},
		autoLootNote = nil,
		columns = {
			attendance = {
				Date = true,
				Attendants = true,
				Tracked = true,
			},
			loot = {
				Date = true,
				Player = true,
				Item = true,
				From = true,
			},
		},
		disallowRanks = {},
		tenMan = true,
		attachTrashToBoss = nil,
	})

	self:UpdateBlacklist()
	self:UpdateWhitelist()
	self:UpdateSpecialItemList()

	self.OnMenuRequest = options

	self:RegisterChatCommand("/nrt", options, "NRT")

	if self.db.profile.dbFormat == 1 then
		self:Print("WARNING: The old date conversion code has been removed, please use an older version of NRT to upgrade your saved data sets and then install the new version again.")
		-- dbFormat should be 2.
	end
end

function addon:OnEnable(first)
	local popup = _G.StaticPopupDialogs
	if type(popup) ~= "table" then
		popup = {}
	end
	if type(popup["NRTBossKilled"]) ~= "table" then
		popup["NRTBossKilled"] = {
			text = "",
			button1 = TEXT(YES),
			button2 = TEXT(NO),
			timeout = 0,
			whileDead = 1,
			hideOnEscape = 1,
			OnAccept = function() self:TakeAttendance() end
		}
	end

	self:RegisterEvent("GUILD_ROSTER_UPDATE")
	self:RegisterEvent("RAID_ROSTER_UPDATE")

	if IsInGuild() then GuildRoster() end
end

-----------------------------------------------------------------------
-- Dynamic menu updating

local function removeWhitelistedUnit(unit)
	addon.db.profile.whitelist[unit] = nil
	options.args.filters.args.whitelist.args[unit] = nil
end

function addon:UpdateWhitelist()
	local opt = options.args.filters.args.whitelist.args
	local db = self.db.profile.whitelist
	for k in pairs(db) do
		if not opt[k] then
			opt[k] = {
				type = "execute",
				name = k,
				desc = L["This player is allowed on the attendance list. Click to remove."],
				func = removeWhitelistedUnit,
				passValue = k,
				order = 10,
			}
		end
	end
end

local function removeBlacklistedUnit(unit)
	addon.db.profile.blacklist[unit] = nil
	options.args.filters.args.blacklist.args[unit] = nil
end

function addon:UpdateBlacklist()
	local opt = options.args.filters.args.blacklist.args
	local db = self.db.profile.blacklist
	for k in pairs(db) do
		if not opt[k] then
			opt[k] = {
				type = "execute",
				name = k,
				desc = L["This player is never allowed on the attendance list. Click to remove."],
				func = removeBlacklistedUnit,
				passValue = k,
				order = 10,
			}
		end
	end
end

local function showSpecialItems(item)
	createTrackerFrame()
	local db = addon.db.profile.specialItems[item]
	if not db then return end
	local text = ""
	for k, v in pairs(db) do
		-- I know this isn't ideal, but what the hell, it happens rarely.
		text = text .. tostring(v) .. "   " .. k .. "\n"
	end
	setText(text)
	local name = GetItemInfo(item)
	setFrameHeader(name or item)
	trackerFrame.trackingId = -1
	trackerFrame:Show()
end

local function clearSpecialItem(item)
	addon.db.profile.specialItems[item] = nil
	addon:UpdateSpecialItemList()
end

-- I'd like to not re-create the entire menu every time, but it's not something
-- I prioritize.
function addon:UpdateSpecialItemList()
	local opt = options.args.special.args
	local db = self.db.profile.specialItems
	for k in pairs(opt) do opt[k] = nil end
	for k in pairs(db) do
		local _, name = GetItemInfo(k)
		if not name then name = "ID: " .. tostring(k) end
		opt[k] = {
			type = "execute",
			name = name,
			desc = L["Show who looted %s and how many they looted."]:format(name),
			func = showSpecialItems,
			passValue = k,
			order = 10,
		}
		opt[k .. "flush"] = {
			type = "execute",
			name = L["Flush %s"]:format(name),
			desc = L["Removes all the stored loot information on %s from the database."]:format(name),
			func = clearSpecialItem,
			passValue = k,
			order = 100,
		}
	end
	if next(opt) then
		opt.spacer = {
			type = "header",
			name = " ",
			order = 50,
		}
	end
end

-----------------------------------------------------------------------
-- Attendance tracking

-- This is probably very expensive :P Oh well.
local function getGameTimeAsUnix()
	local h, m = GetGameTime()
	local n = date("*t")
	n.hour = h
	n.min = m
	return time(n)
end

local function IsAllowedOnAttendanceSets(input)
	local db = addon.db.profile
	if db.whitelist[input] then return true end
	if db.blacklist[input] then return false end
	if memberRanks[input] and db.disallowRanks[memberRanks[input]] then return false end
	if UnitInRaid(input) or guildMemberList[input] then return true end
	return false
end

local function updateWaitingList(name)
	for i, v in ipairs(waitingList) do
		if v:find(name) then return end
	end
	table_insert(waitingList, coloredName[name])
end

local function whisperHandler()
	-- Lower the whole string then trim it and then uppercase the first
	-- letter to give us what might be closest to an actual player name.
	local text = arg1

	-- Only sanitize if we have to.
	if not IsAllowedOnAttendanceSets(text) then
		text = arg1:lower():trim():gsub("^.", string.upper)
	end

	if text:find("%s") or text:find("%d") then return end

	local player = arg2
	if IsAllowedOnAttendanceSets(text) then
		local set = addon.db.profile.tracked[currentTrackset]
		if set then
			local found = nil
			for i, v in ipairs(set.attendants) do
				if v == text then
					found = true
					break
				end
			end
			if not found then
				updateWaitingList(text)
				table_insert(set.attendants, text)
				SendChatMessage(L["<NRT>: %q has been added to the attendance list."]:format(text), "WHISPER", nil, player)
				addon:Print(L["%q has been added to the attendance list by %s."]:format(coloredName[text], coloredName[player]))
				if trackerFrame and trackerFrame:IsShown() and trackerFrame.trackingId == currentTrackset then
					addon:ShowTrackerForRaidIndex(currentTrackset)
				end
				return true
			end
		end
	else
		if this and this ~= ChatFrame1 then return end
		SendChatMessage(L["<NRT>: %q can't be added to this attendance set."]:format(text), "WHISPER", nil, player)
		return true
	end
end

local function whisperInformHandler(msg)
	if msg:find("^<NRT>") then return true end
end

local divider = ("*"):rep(10)
function addon:TakeAttendance()
	if self:IsEventScheduled("Timeout") then
		self:Print(L["You're already taking attendance, please wait."])
		return
	end

	local newTrackset = {
		name = lastBossKilled or GetRealZoneText(),
		zone = GetRealZoneText(),
		attendants = {},
		date = getGameTimeAsUnix(),
	}
	for i = 1, GetNumRaidMembers() do
		local n, _, subgroup = GetRaidRosterInfo(i)
		if IsAllowedOnAttendanceSets(n) and subgroup <= self.db.profile.groups then
			table_insert(newTrackset.attendants, n)
		end
	end
	table_insert(self.db.profile.tracked, newTrackset)
	currentTrackset = #self.db.profile.tracked

	if self.db.profile.announceAttendance then
		ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", whisperHandler)
		ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", whisperInformHandler)

		local str = nil
		if lastBossKilled then
			str = L["NRT: %s down! If you're not in the raid whisper me your mains name to be added to the DKP list."]:format(lastBossKilled)
		else
			str = L["NRT: If you're not in the raid whisper me your mains name to be added to the DKP list."]
		end

		if self.db.profile.guildAttendance then
			SendChatMessage(divider, "GUILD")
			SendChatMessage(str, "GUILD")
			SendChatMessage(divider, "GUILD")
		end

		if self.db.profile.customAttendance then
			local id = GetChannelName(self.db.profile.customAttendance)
			if id then
				SendChatMessage(divider, "CHANNEL", nil, id)
				SendChatMessage(str, "CHANNEL", nil, id)
				SendChatMessage(divider, "CHANNEL", nil, id)
			end
		end

		self:ScheduleEvent("Timeout", self.AttendanceTimeout, self.db.profile.announceTimeout * 60, self)

		if type(ShutUp_Disable) == "function" then
			ShutUp_Disable()
		end
	else -- We're not announcing anything, so just clean up right away.
		self:AttendanceTimeout()
	end
end

function createTrackerFrame()
	if trackerFrame then return end

	trackerFrame = CreateFrame("Frame", "NRTTrackerFrame", UIParent)
	trackerFrame:Hide()

	trackerFrame:SetWidth(addon.db.profile.trackerFrameWidth)
	trackerFrame:SetHeight(addon.db.profile.trackerFrameHeight)

	-- thanks to haste for the style
	trackerFrame:SetBackdrop({
		bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16,
		edgeFile = "Interface\\AddOns\\NRT\\textures\\otravi-semi-full-border", edgeSize = 32,
		insets = {left = 1, right = 1, top = 20, bottom = 1},
	})
	trackerFrame:SetBackdropColor(24/255, 24/255, 24/255)
	trackerFrame:ClearAllPoints()
	trackerFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
	trackerFrame:EnableMouse(true)
	trackerFrame:RegisterForDrag("LeftButton")
	trackerFrame:SetMovable(true)
	trackerFrame:SetResizable(true)
	trackerFrame:SetMinResize(280,300)
	trackerFrame:SetScript("OnDragStart", function()
		this:StartMoving()
	end)
	trackerFrame:SetScript("OnDragStop", function()
		this:StopMovingOrSizing()
		local s = this:GetEffectiveScale()

		addon.db.profile.posX = this:GetLeft() * s
		addon.db.profile.posY = this:GetTop() * s
	end)
	trackerFrame:SetScript("OnSizeChanged", function()
		addon.db.profile.trackerFrameWidth = this:GetWidth()
		addon.db.profile.trackerFrameHeight = this:GetHeight()
	end)

	local cheader = trackerFrame:CreateFontString(nil,"OVERLAY")
	cheader:ClearAllPoints()
	cheader:SetWidth(240)
	cheader:SetHeight(15)
	cheader:SetPoint("TOPLEFT", trackerFrame, "TOPLEFT", 7, -14)
	cheader:SetPoint("TOPRIGHT", trackerFrame, "TOPRIGHT", -28, -14)
	cheader:SetFont("Fonts\\FRIZQT__.TTF", 12)
	cheader:SetJustifyH("LEFT")
	setFrameHeader = function(text)
		cheader:SetText(text)
	end
	cheader:SetText("Tracker")
	cheader:SetShadowOffset(.8, -.8)
	cheader:SetShadowColor(0, 0, 0, 1)

	local close = trackerFrame:CreateTexture(nil, "ARTWORK")
	close:SetTexture("Interface\\AddOns\\NRT\\textures\\otravi-close")
	close:SetTexCoord(0, .625, 0, .9333)
	close:SetWidth(20)
	close:SetHeight(14)
	close:SetPoint("TOPRIGHT", trackerFrame, "TOPRIGHT", -7, -15)

	local closebutton = CreateFrame("Button", nil)
	closebutton:SetParent(trackerFrame)
	closebutton:SetWidth(20)
	closebutton:SetHeight(14)
	closebutton:SetPoint("CENTER", close, "CENTER")
	closebutton:SetScript("OnClick", function()
		trackerFrame:Hide()
		trackerFrame.trackingId = nil
	end)

	local scroll = CreateFrame("ScrollFrame", "NRTScrollFrame", trackerFrame, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", trackerFrame, "TOPLEFT", 20, -40)
	scroll:SetPoint("RIGHT", trackerFrame, "RIGHT", -40, 0)
	scroll:SetPoint("BOTTOM", trackerFrame, 0, 20)

	local textField = CreateFrame("EditBox", "NRTEditBox", scroll)
	textField:SetFontObject(ChatFontNormal)
	textField:SetMultiLine(true)
	textField:SetWidth(240)
	textField:SetScript("OnEscapePressed", function() this:ClearFocus() end)
	textField:SetScript("OnTextChanged", function() scroll:UpdateScrollChildRect() end)
	textField:SetText("")
	setText = function(text)
		textField:SetText(text)
	end
	textField:SetAutoFocus(false)
	scroll:SetScrollChild(textField)

	-- drag handle, thanks to ammo
	local draghandle = CreateFrame("Frame", nil, trackerFrame)
	draghandle:Hide()
	draghandle:SetFrameLevel(trackerFrame:GetFrameLevel() + 10)
	draghandle:SetWidth(16)
	draghandle:SetHeight(16)
	draghandle:SetPoint("BOTTOMRIGHT", trackerFrame, "BOTTOMRIGHT", -1, 1)
	draghandle:EnableMouse(true)
	draghandle:SetScript("OnMouseDown", function()
		trackerFrame:StartSizing("BOTTOMRIGHT")
	end)
	draghandle:SetScript("OnMouseUp", function()
		trackerFrame:StopMovingOrSizing()
	end)
	draghandle:SetScript("OnEnter", function()
		addon:CancelScheduledEvent("NRTHideDrag")
	end)
	draghandle:SetScript("OnLeave", function()
		this:Hide()
	end)

	local texture = draghandle:CreateTexture(nil,"BACKGROUND")
	texture:SetTexture("Interface\\AddOns\\NRT\\textures\\draghandle")
	texture:SetWidth(16)
	texture:SetHeight(16)
	texture:SetBlendMode("ADD")
	texture:SetPoint("CENTER", draghandle, "CENTER", 0, 0)

	trackerFrame:SetScript("OnEnter", function()
		draghandle:Show()
	end)
	local function hideHandle()
		draghandle:Hide()
	end
	trackerFrame:SetScript("OnLeave", function()
		addon:ScheduleEvent("NRTHideDrag", hideHandle, 1)
	end)

	local x = addon.db.profile.posX
	local y = addon.db.profile.posY
	if x and y then
		local s = trackerFrame:GetEffectiveScale()
		trackerFrame:ClearAllPoints()
		trackerFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x / s, y / s)
	end
end

-----------------------------------------------------------------------
-- Event handling

local function checkTarget(target)
	local class = UnitClassification(target)
	if class and class == "worldboss" then
		worldbosses[UnitName(target)] = true
	end
end

function addon:PLAYER_TARGET_CHANGED()
	checkTarget("target")
end

function addon:UPDATE_MOUSEOVER_UNIT()
	checkTarget("mouseover")
end

do
	local updating = nil
	function addon:GUILD_ROSTER_UPDATE()
		if updating then return end
		updating = true
		if self:IsEventRegistered("GUILD_ROSTER_UPDATE") then
			self:UnregisterEvent("GUILD_ROSTER_UPDATE")
		end

		for k in pairs(guildRanks) do guildRanks[k] = nil end
		for i = 1, GuildControlGetNumRanks() do
			table_insert(guildRanks, GuildControlGetRankName(i))
		end
		local offline = GetGuildRosterShowOffline()
		local selection = GetGuildRosterSelection()
		SetGuildRosterShowOffline(true)
		local numGuildMembers = GetNumGuildMembers()
		for i = 1, numGuildMembers do
			local name, rank, _, _, _, _, _, _, _, _, class = GetGuildRosterInfo(i)
			if name then
				if rawget(coloredName, name) then
					coloredName[name] = nil
				end
				guildMemberList[name] = class
				memberRanks[name] = rank
			end
		end
		SetGuildRosterShowOffline(offline)
		SetGuildRosterSelection(selection)

		updating = nil
		self:RegisterEvent("GUILD_ROSTER_UPDATE")
	end
end

function addon:AttendanceTimeout()
	ChatFrame_RemoveMessageEventFilter("CHAT_MSG_WHISPER", whisperHandler)
	ChatFrame_RemoveMessageEventFilter("CHAT_MSG_WHISPER_INFORM", whisperInformHandler)

	if self.db.profile.announceAttendance then
		local str = L["NRT: If you haven't whispered me by now, you're too late."]
		if self.db.profile.guildAttendance then
			SendChatMessage(str, "GUILD")
		end
		if self.db.profile.customAttendance then
			local id = GetChannelName(self.db.profile.customAttendance)
			if id then
				SendChatMessage(str, "CHANNEL", nil, id)
			end
		end
		if type(ShutUp_Enable) == "function" then
			ShutUp_Enable()
		end
	end
	currentTrackset = nil
end

function addon:RAID_ROSTER_UPDATE()
	local num = GetNumRaidMembers()
	for u = 1, num do
		local n = GetRaidRosterInfo(u)
		for i, v in ipairs(waitingList) do
			if v == n or v:find(n) then
				table.remove(waitingList, i)
				break
			end
		end
	end
	local c = self.db.profile.tenMan and 0 or 10
	if not grouped and num > c then
		grouped = true
		self:Print(L["Tracking boss kills and loot."])
		self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
		self:RegisterEvent("CHAT_MSG_LOOT")
		self:RegisterEvent("PLAYER_TARGET_CHANGED")
		self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
	elseif grouped and num <= c then
		grouped = nil
		self:Print(L["Tracking disabled."])
		self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
		self:UnregisterEvent("CHAT_MSG_LOOT")
		self:UnregisterEvent("PLAYER_TARGET_CHANGED")
		self:UnregisterEvent("UPDATE_MOUSEOVER_UNIT")
	end

	self:UpdateText()
end

local currentNoteItem = nil
local itemNoteText = L["Set note for %s.\n\nSetting a note sets it for all instances - past, present and future - of this item, not just this particular one."]
local function itemNote(item)
	if type(item) == "number" then
		currentNoteItem = item
	else
		currentNoteItem = tonumber(select(3, item:find("item:(%d+):")))
	end
	if not StaticPopupDialogs["NRTItemNoteDialog"] then
		StaticPopupDialogs["NRTItemNoteDialog"] = {
			text = nil,
			button1 = L["Set"],
			button2 = L["Cancel"],
			whileDead = 1,
			hideOnEscape = 1,
			timeout = 0,
			OnShow = function()
				-- We have to do this onshow to reset the previous text
				_G[this:GetName().."EditBox"]:SetText(addon.db.profile.itemNotes[currentNoteItem] or "")
			end,
			OnHide = function()
				_G[this:GetName().."EditBox"]:SetText("")
			end,
			EditBoxOnEnterPressed = function()
				local input = _G[this:GetParent():GetName().."EditBox"]:GetText()
				addon.db.profile.itemNotes[currentNoteItem] = input
				this:GetParent():Hide()
			end,
			EditBoxOnEscapePressed = function()
				this:GetParent():Hide()
			end,
			OnAccept = function()
				local input = _G[this:GetParent():GetName().."EditBox"]:GetText()
				addon.db.profile.itemNotes[currentNoteItem] = input
			end,
			hasEditBox = 1,
		}
	end
	StaticPopupDialogs["NRTItemNoteDialog"].text = itemNoteText:format(item)
	StaticPopup_Show("NRTItemNoteDialog")
end

function addon:ItemLooted(player, item, chatMsg)
	local itemName, itemLink, itemRarity = GetItemInfo(item)

	-- Find the item ID to make sure it's not a ignored item
	local itemId = select(3, itemLink:find("item:(%d+):"))
	if not itemId then return false end
	itemId = tonumber(itemId:trim())
	if type(itemId) ~= "number" then return false end
	if ignoredItems[itemId] or (itemRarity < 4 and not specialItems[itemId]) then return false end

	if specialItems[itemId] then
		-- Figure out how many they looted.
		local amount
		if chatMsg and player == UnitName("player") then
			amount = select(2, deformat(chatMsg, LOOT_ITEM_SELF_MULTIPLE))
		elseif chatMsg then
			amount = select(3, deformat(chatMsg, LOOT_ITEM_MULTIPLE))
		end
		amount = tonumber(amount)
		if type(amount) ~= "number" then amount = 1 end
		if not self.db.profile.specialItems[itemId] then self.db.profile.specialItems[itemId] = {} end
		if not self.db.profile.specialItems[itemId][player] then self.db.profile.specialItems[itemId][player] = 0 end
		self.db.profile.specialItems[itemId][player] = self.db.profile.specialItems[itemId][player] + amount
		self:UpdateSpecialItemList()
	else
		local targetId = nil
		for i = 1, GetNumRaidMembers() do
			if GetRaidRosterInfo(i) == player then
				targetId = "raid" .. i .. "target"
				break
			end
		end

		local lootedFrom = nil
		local c = UnitClassification(targetId)
		if not self.db.profile.attachTrashToBoss and UnitExists(targetId) and (c == "worldboss" or c == "elite") then
			lootedFrom = UnitName(targetId)
		elseif lastBossKilled then
			lootedFrom = lastBossKilled
		else
			lootedFrom = GetRealZoneText()
		end

		if self.db.profile.announceLoot then
			SendChatMessage(L["%s looted %s (%s)!"]:format(player, itemLink, lootedFrom), "GUILD")
		end

		table_insert(self.db.profile.loot, {
			player = player,
			item = itemLink,
			itemId = itemId,
			from = lootedFrom,
			date = getGameTimeAsUnix(),
		})

		if self.db.profile.autoLootNote then
			itemNote(itemLink)
		end

		if self.db.profile.attendanceOnLoot then
			if not self.db.profile.suppressPopup then
				_G.StaticPopupDialogs["NRTBossKilled"].text = L["%s looted. Take raid attendance now?"]:format(itemLink)
				_G.StaticPopup_Show("NRTBossKilled")
			else
				self:Print(L["%s looted. Taking attendance now..."]:format(itemLink))
				self:TakeAttendance()
			end
		end
	end

	return true
end

function addon:CHAT_MSG_LOOT(msg)
	local player, item = deformat(msg, LOOT_ITEM)
	if not player then
		item = deformat(msg, LOOT_ITEM_SELF)
		if item then
			player = UnitName("player")
		end
	end
	if type(item) == "string" and type(player) == "string" then
		self:ItemLooted(player, item, msg)
	end
end

local function ResetBossKilled()
	lastBossKilled = nil
end

function addon:COMBAT_LOG_EVENT_UNFILTERED(_, event, _, _, _, _, mob)
	if event ~= "UNIT_DIED" then return end
	if mob and worldbosses[mob] and not ignoredBosses[mob] then
		table_insert(killedBosses, mob)

		lastBossKilled = mob
		if not self.db.profile.attachTrashToBoss then
			self:ScheduleEvent("ResetBossKilled", ResetBossKilled, 300)
		end

		if self.db.profile.bossKill then
			if not self.db.profile.suppressPopup then
				_G.StaticPopupDialogs["NRTBossKilled"].text = L["%s killed. Take raid attendance now?"]:format(mob)
				_G.StaticPopup_Show("NRTBossKilled")
			else
				self:Print(L["%s killed. Taking attendance now..."]:format(mob))
				self:TakeAttendance()
			end
		end
	end
end

-----------------------------------------------------------------------
-- UI Handling

function addon:ClearAll()
	self.db.profile.loot = {}
	self.db.profile.tracked = {}
	self.db.profile.specialItems = {}

	self:UpdateSpecialItemList()
	self:UpdateDisplay()
end

local function itemClickFunc(item, index)
	if type(index) ~= "number" or type(item) ~= "string" then return end

	if IsShiftKeyDown() then
		table.remove(addon.db.profile.loot, index)
	elseif IsControlKeyDown() then
		itemNote(item)
	elseif IsAltKeyDown() then
		ChatFrameEditBox:Show()
		ChatFrameEditBox:Insert(item)
	else
		SetItemRef(item)
	end
end

function addon:ShowTrackerForRaidIndex(index)
	createTrackerFrame()

	local raid = self.db.profile.tracked[index]
	if not raid then return end

	local formatter = formatters[self.db.profile.formatter]
	if not formatter then
		self:Print(L["The formatter %s isn't registered."]:format(self.db.profile.formatter))
		return
	end
	local text = formatter(raid, self.db.profile.loot, killedBosses)
	if not text then
		self:Print(L["The formatter %s didn't return any text for the raid ID %d."]:format(self.db.profile.formatter, index))
		return
	end

	setText(text)
	setFrameHeader(raid.date .. " " .. raid.name)
	trackerFrame.trackingId = index
	trackerFrame:Show()
	NRTEditBox:SetFocus()
	NRTEditBox:HighlightText()
end

local currentTrack = nil
local trackNoteText = L["Set note for track ID %d."]
local function trackerClickFunc(trackId)
	if type(trackId) ~= "number" then return end

	if IsShiftKeyDown() then
		if addon.db.profile.tracked[trackId] then
			table.remove(addon.db.profile.tracked, trackId)
		end
	elseif IsControlKeyDown() and addon.db.profile.tracked[trackId] then
		currentTrack = trackId
		if not StaticPopupDialogs["NRTTrackedNoteDialog"] then
			StaticPopupDialogs["NRTTrackedNoteDialog"] = {
				text = nil,
				button1 = L["Set"],
				button2 = L["Cancel"],
				whileDead = 1,
				hideOnEscape = 1,
				timeout = 0,
				OnShow = function()
					-- We have to do this onshow to reset the previous text
					_G[this:GetName().."EditBox"]:SetText(addon.db.profile.tracked[currentTrack].note or "")
				end,
				OnHide = function()
					_G[this:GetName().."EditBox"]:SetText("")
				end,
				EditBoxOnEnterPressed = function()
					local input = _G[this:GetParent():GetName().."EditBox"]:GetText()
					addon.db.profile.tracked[currentTrack].note = input
					this:GetParent():Hide()
				end,
				EditBoxOnEscapePressed = function()
					this:GetParent():Hide()
				end,
				OnAccept = function()
					local input = _G[this:GetParent():GetName().."EditBox"]:GetText()
					addon.db.profile.tracked[currentTrack].note = input
				end,
				hasEditBox = 1,
			}
		end
		StaticPopupDialogs["NRTTrackedNoteDialog"].text = trackNoteText:format(currentTrack)
		StaticPopup_Show("NRTTrackedNoteDialog")
	else
		-- Show the text area with the details from this track
		addon:ShowTrackerForRaidIndex(trackId)
	end
end

do
	local tabletData = {
		attendance = {
			category = {},
			functions = {},
		},
		loot = {
			category = {},
			functions = {},
		},
	}
	local function columnSorter(a, b)
		if columnPriority[a] < columnPriority[b] then return true
		else return false end
	end
	local function insert(tbl, ...)
		for i = 1, select("#", ...) do
			table_insert(tbl, (select(i, ...)))
		end
	end
	local function refixColumns(tbl, func)
		for k in pairs(tabletData[tbl].category) do tabletData[tbl].category[k] = nil end
		for k in pairs(tabletData[tbl].functions) do tabletData[tbl].functions[k] = nil end
		local cols = {}
		for k, v in pairs(addon.db.profile.columns[tbl]) do
			if v then table_insert(cols, k) end
		end
		if #cols > 0 then
			table_sort(cols, columnSorter)
			insert(tabletData[tbl].category, "columns", #cols, "showWithoutChildren", false, "child_func", func)
			for i = 1, #cols do
				local n = i == 1 and "" or tostring(i)
				insert(tabletData[tbl].functions, "text" .. n, columnTextFunctions[tbl][cols[i]])
				insert(tabletData[tbl].category, "text" .. n, L[cols[i]])
				if columnColors[cols[i]] then
					insert(
						tabletData[tbl].category,
						"child_text"..n.."R", columnColors[cols[i]][1],
						"child_text"..n.."G", columnColors[cols[i]][2],
						"child_text"..n.."B", columnColors[cols[i]][3]
					)
				end
			end
		end
	end

	function addon:OnDoubleClick()
		self:TakeAttendance()
	end

	function addon:OnTextUpdate()
		if grouped then
			self:SetText("|cff00ff00NRT|r")
		else
			self:SetText("|cffff0000NRT|r")
		end
	end

	-- XXX Hopefully I can optimize this at a later point.
	local tmp = {}
	local function callUnpack(input, ...)
		for k in pairs(tmp) do tmp[k] = nil end
		for i = 1, select("#", ...) do
			local x = select(i, ...)
			if type(x) == "function" then
				table_insert(tmp, x(input))
			else
				table_insert(tmp, x)
			end
		end
		return unpack(tmp)
	end

	function addon:OnTooltipUpdate()
		if updateUIColumnList then
			refixColumns("attendance", trackerClickFunc)
			refixColumns("loot", itemClickFunc)
			updateUIColumnList = nil
		end
		if #self.db.profile.tracked > 0 then
			local d = tabletData.attendance
			if #d.category > 0 then
				local cat = tablet:AddCategory(unpack(d.category))
				for i, v in ipairs(self.db.profile.tracked) do
					cat:AddLine("arg1", i, callUnpack(v, unpack(d.functions)))
				end
			end
		end
		if #self.db.profile.loot > 0 then
			local d = tabletData.loot
			if #d.category > 0 then
				local cat = tablet:AddCategory(unpack(d.category))
				for i, v in ipairs(self.db.profile.loot) do
					cat:AddLine("arg1", v.item, "arg2", i, callUnpack(v, unpack(d.functions)))
				end
			end
		end
		tablet:SetHint(L["|cffeda55fDouble-Click|r to take attendance now. |cffeda55fClick|r an item to show. |cffeda55fShift-Click|r to remove. |cffeda55fCtrl-Click|r to edit notes. |cffeda55fAlt-Click|r an item to insert it into the chat box."])
	end
end

-----------------------------------------------------------------------
-- Formatters

function addon:RegisterOutputFormatter(name, formatter)
	if type(name) ~= "string" or type(formatter) ~= "function" then
		error(("Invalid arguments to :RegisterOutputFormatter, tried registering a formatter without a proper name (%q) or function (%q)."):format(type(name), type(formatter)), 2)
	end
	if formatters[name] then
		error(("Invalid argument to :RegisterOutputFormatter, there's already an output formatter named %q registered."):format(name), 2)
	end
	table_insert(outputFormatters, name)
	formatters[name] = formatter
end

-- Default formatter
addon:RegisterOutputFormatter("Nihilum", function(attendance, loot, killedBosses)
	return table.concat(attendance.attendants, "\n")
end)

addon:RegisterOutputFormatter("Example XML", function(attendance, loot, killedBosses)
	local lootByTracked = {}
	for i, v in ipairs(loot) do
		if not lootByTracked[v.from] then lootByTracked[v.from] = {} end
		table_insert(lootByTracked[v.from], v)
	end

	local text = "<raid zone=\"" .. attendance.zone .. "\">\n  <date>" .. date("%d/%m %H:%M", attendance.date) .. "</date>\n  "
	if attendance.note then text = text .. "<note>" .. attendance.note .. "</note>\n  " end
	for k, v in pairs(lootByTracked) do
		text = text .. "<tracked source=\"" .. k .. "\">"
		for i, item in ipairs(v) do
			local itemName, _, itemRarity = GetItemInfo(item.item)
			local id = item.itemId or tonumber(item.item:match("^|%x+|Hitem:(%d+):"))
			local note = addon.db.profile.itemNotes[id] or ""
			text = text .. "\n    <item date=\"" .. date("%d/%m %H:%M", item.date) .. "\" player=\"" .. item.player .. "\" name=\"" .. (itemName or "?") .. "\" quality=\"" .. (itemRarity or "?") .. "\" id=\"" .. id .. "\" note=\"" .. note .. "\" />"
		end
		text = text .. "\n  </tracked>\n  "
	end
	text = text .. "<players>"
	for i, v in ipairs(attendance.attendants) do
		text = text .. "\n    <player>" .. v .. "</player>"
	end
	text = text .. "\n  </players>\n  <bosses>"
	for i, v in ipairs(killedBosses) do
		text = text .. "\n    <boss>" .. v .. "</boss>"
	end
	text = text .. "\n  </bosses>\n</raid>"
	return text
end)

local ifDateFormat = "%Y-%m-%d %H:%M:%S"
addon:RegisterOutputFormatter("Inner Focus", function(attendance, loot, killedBosses)
	local dkpString = "<raid>\n"
	local bossKilled = "  <boss>\n    <name>" .. attendance.name .. "</name>\n    <date>" .. date(ifDateFormat, attendance.date ) .. "</date>\n"	
	local playerList = "    <players>\n"

	for i, v in ipairs(attendance.attendants) do
		playerList = playerList .. "      <player>" .. v .. "</player>\n"
	end
	-- Loot Start
	local lootList = "    <loots>\n"
	local lootByTracked = {}
	for i, v in ipairs(loot) do
		if not lootByTracked[v.from] then lootByTracked[v.from] = {} end
		table_insert(lootByTracked[v.from], v)
	end

	for k, v in pairs(lootByTracked) do
		if k == attendance.name then
			lootList = lootList .. "      <loot source=\"" .. k .. "\">"
			for i, item in ipairs(v) do
				local itemName, _, itemRarity = GetItemInfo(item.item)
				local id = item.itemId or tonumber(item.item:match("^|%x+|Hitem:(%d+):"))
				local note = addon.db.profile.itemNotes[id] or ""
				lootList = lootList .. "\n        <item date=\"" .. date(ifDateFormat, item.date) .. "\" player=\"" .. item.player .. "\" name=\"" .. (itemName or "?") .. "\" quality=\"" .. (itemRarity or "?") .. "\" id=\"" .. id .. "\" note=\"" .. note .. "\" />"
			end
		lootList = lootList .. "\n      </loot>\n"
		end
	end
	lootList = lootList .. "    </loots>\n"
	-- Loot End

	playerList = playerList .. "    </players>\n"
	dkpString = dkpString .. bossKilled .. playerList .. lootList

	dkpString = dkpString .. "  </boss>\n</raid>"
	return dkpString
end)

local ctrtDateFormat = "%m/%d/%y %H:%M:%S"
addon:RegisterOutputFormatter("CT Raidtracker XML", function(attendance, loot, killedBosses)
	local lootByTracked = {}
	for i, v in ipairs(loot) do
		if not lootByTracked[v.from] then lootByTracked[v.from] = {} end
		table_insert(lootByTracked[v.from], v)
	end

	local text = "<RaidInfo>"
	local players = ""

	local fdate = date(ctrtDateFormat, attendance.date)

	text = text .. "<key>" .. fdate .. "</key>"
	text = text .. "<start>" .. fdate .. "</start>"
	text = text .. "<end>" .. fdate .. "</end>"
	text = text .. "<zone>" .. attendance.zone .. "</zone>"

	text = text .. "<PlayerInfos>"

	for i, v in ipairs(attendance.attendants) do
		text = text .. "<key" .. i .. "><name>" .. v .. "</name></key" .. i .. ">"
		players = players .. "<key" .. i .. "><player>" .. v .. "</player><time>" .. fdate .. "</time></key" .. i .. ">"
	end

	text = text .. "</PlayerInfos>"

	text = text .. "<BossKills>"
	for i, v in ipairs(killedBosses) do
		text = text .. "<key" .. i .. "><name>" .. v .. "</name><time>" .. fdate .. "</time><attendees/></key" .. i .. ">"
	end
	text = text .. "</BossKills>"

	-- Add in attendance.note here somewhere?
	text = text .. "<note><![CDATA[" .. attendance.zone .. " - " .. attendance.name .. " - " .. (attendance.note or "") .. "]]></note>"
	text = text .. "<Join>" .. players .. "</Join>"
	text = text .. "<Leave>" .. players .. "</Leave>"

	text = text .. "<Loot>"

	for k, v in pairs(lootByTracked) do
		if k == attendance.name then
			for i, item in ipairs(v) do
				local itemName = GetItemInfo(item.item)
				local id = item.item:match("^|%x+|Hitem:(.*)")
				id = id:sub(1, id:find("|") - 1)

				local color = string.sub(item.item:match("^|(%x+)|"), 2)
				local note = addon.db.profile.itemNotes[item.itemId] or ""

				text = text .. "<key" .. i .. ">"
				text = text .. "<ItemName>" .. (itemName or "?") .. "</ItemName>"
				text = text .. "<ItemID>" .. id .. "</ItemID>"
				text = text .. "<Icon></Icon>"
				text = text .. "<Class></Class>"
				text = text .. "<SubClass></SubClass>"
				text = text .. "<Color>" .. color .. "</Color>"
				text = text .. "<Count>1</Count>"
				text = text .. "<Player>" .. item.player .. "</Player>"
				text = text .. "<Time>" ..  date(ctrtDateFormat, item.date) .. "</Time>"
				text = text .. "<Zone></Zone>"
				text = text .. "<Boss>" .. item.from .. "</Boss>"
				text = text .. "<Note>" .. note .. "</Note>"
				text = text .. "</key" .. i .. ">"
			end
		end
	end

	text = text .. "</Loot>"
	text = text .. "</RaidInfo>"
	return text
end)

